Skip to content

Conversation

@Keinberger
Copy link
Collaborator

@Keinberger Keinberger commented Jan 7, 2026

Updates


Summary

Closes #1639

This PR implements a from_system_user_config() method for CliClient that creates a Client instance using the same configuration as the CLI tool (reading from .miden/miden-client.toml or local config). This enables anyone to programmatically obtain a properly configured CLI client.

This change is required for implementing the mint command https://github.com/0xMiden/midenup/issues/128, in particular with the new implementation strategy described in 0xMiden/midenup#130 (comment). The https://github.com/0xMiden/miden-faucet repository needs to create a CLI-configured client instance to consume notes and sync state after making the mint request when using the miden-faucet-client, see this PR description for more details: 0xMiden/miden-faucet#196.

Details

New CliClient wrapper type

A newtype wrapper CliClient(Client<CliKeyStore>) has been added to miden-client-cli with Deref and DerefMut implementations. This allows external projects to:

  1. Initialize a client with CLI configuration via CliClient::from_system_user_config()
  2. Use all Client methods transparently through deref coercion
  3. Access the inner Client<CliKeyStore> when needed via into_inner(), inner(), or inner_mut()

Configuration loading behavior

The from_system_user_config() method searches for configuration files in order:

  1. Local .miden/miden-client.toml in the current working directory
  2. Global .miden/miden-client.toml in the home directory

The client is initialized with the same settings as the CLI:

  • SQLite store from configured path
  • gRPC client connection to configured RPC endpoint
  • Filesystem-based keystore authenticator
  • Optional note transport client (if configured)
  • Transaction graceful blocks delta
  • Optional max block number delta
  • Debug mode (accepts DebugMode enum, matching ClientBuilder::in_debug_mode() API)

The new CliClient has been integrated into the existing CLI logic to ensure that the CliClient is the single source of configuration for the CLI client instance.

Usage example

use miden_client::DebugMode;
use miden_client_cli::CliClient;

// Create a CLI-configured client
let client = CliClient::from_system_user_config(DebugMode::Disabled).await?;

// Use it like a regular Client
client.sync_state().await?;

// Build and submit transactions
let req = TransactionRequest::builder()
    .consume_notes(recently_minted_note)
    .build()?;

client.submit_new_transaction(req, target_account_id)?;

This pattern was originally described by @igamigo in the following comment: 0xMiden/midenup#130 (comment)

@Keinberger Keinberger requested a review from igamigo January 7, 2026 13:57
@Keinberger Keinberger linked an issue Jan 7, 2026 that may be closed by this pull request
@Keinberger Keinberger self-assigned this Jan 7, 2026
@Keinberger Keinberger marked this pull request as draft January 7, 2026 14:02
@Keinberger Keinberger marked this pull request as ready for review January 7, 2026 16:13
@Keinberger Keinberger requested a review from mmagician January 7, 2026 16:13
@Keinberger Keinberger changed the title feat: implement and export CliClient for default Client initializatio… feat: implement CliClient Jan 7, 2026
Comment on lines 1312 to 1314
// Verify the client is functional by syncing
let sync_result = client.sync_state().await;
assert!(sync_result.is_ok(), "Client sync failed: {:?}", sync_result.err());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doesn't really assert that the test ends up using the local config (although this might not be simple anyway)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies for the global one

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think unfortuantely there is no good way to test this. The assertions didn't actually verify which config was used.

I've fixed this by looking at the actual store file path that was created.

Comment on lines 1333 to 1335
// Attempt to create a client (should fail)
let client = miden_client_cli::CliClient::from_system_user_config(DebugMode::Disabled).await;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not silently initialize the client? Or is that not ideal?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely correct, I missed that one. I implemented that now!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the implementation to call InitCmd::default().execute() when no config is found, and adjusted the test to verify silent initialization works correctly.

/// # Ok(())
/// # }
/// ```
pub async fn from_system_user_config(debug_mode: DebugMode) -> Result<Self, CliError> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that would be nice is passing a CliConfig to a client constructor, and then have different constructors/loaders for CliConfig (for example, one for local config, one for global, one that respects the priority); it can also save the TOML automatically on the correct directories. We can still keep this from_system_user_config() as well.
I think this also makes tests better in the sense that you don't have to change the working dir.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree with your suggestion! However, are you fine if we move this to a separate issue to speed up this PR and the mint command (mint has taken quite long already)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I addressed this too and refactored the CLI config

Comment on lines 1293 to 1295
// Change to the temp directory to ensure the local config is picked up
let original_dir = env::current_dir().unwrap();
env::set_current_dir(&temp_dir)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is also process-wide which could be problematic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed according with #1642 (comment)

@Keinberger
Copy link
Collaborator Author

Update

Hey everyone, I made a commit with several changes to address the code feedback from @igamigo .

Here's what changed:

  • Config separation architecture: Split configuration loading from client construction. Added CliConfig::from_system(), from_dir(), from_local_dir(), and from_global_dir() methods. Added CliClient::from_config() to build client from explicit config.

  • Test changes: Removed all env::set_current_dir() calls from tests (process-wide side effect). Tests now use explicit paths via CliConfig::from_dir().

  • Safety warnings: Added prominent "⚠️ WARNING: Advanced Use Only" documentation to from_dir(), from_local_dir(), from_global_dir(), and from_config(). These methods bypass CLI's standard config discovery logic (local → global priority). I think this is important: without warnings, external developers might use these testing/advanced methods in production, causing their apps to behave differently than the CLI tool (e.g., ignoring local configs). Marked from_system() and from_system_user_config() as "✅ Recommended" for standard use. The emoji usage serves as eye-catchers to catch awareness when reading the docs.

  • Silent initialization: from_system_user_config() now auto-initializes config if none exists, matching the standard CLI behavior.

Copy link
Collaborator

@igamigo igamigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I left some comments, most of which are minor. If you prefer to tackle them separately, let's open an issue for them before merging this.

Comment on lines 87 to 91
///
/// # Deprecated
/// This function is deprecated in favor of `CliConfig::from_system()` which provides
/// the same functionality with a cleaner API.
pub(super) fn load_config_file() -> Result<(CliConfig, PathBuf), CliError> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just delete this and use the new functions instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Deleted load_config_file() from utils.rs and replaced all usages with CliConfig::from_system() directly

///
/// # Returns
///
/// A configured `CliClient` instance.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: These can be doc-linked if you use [] instead:

Suggested change
/// A configured `CliClient` instance.
/// A configured [`CliClient`] instance.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens throughout the file. It's not critical though so if you prefer the current state that's fine too

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Updated all documentation to use proper doc-links like [CliClient], [CliConfig],[CliError],[DebugMode::Enabled], etc. Also added cross-references between related methods.

@igamigo
Copy link
Collaborator

igamigo commented Jan 13, 2026

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

@Keinberger
Copy link
Collaborator Author

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

Added pub use miden_client as client. All miden-client types can now be accessed through miden_client_cli::client::*

@igamigo
Copy link
Collaborator

igamigo commented Jan 15, 2026

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

Added pub use miden_client as client. All miden-client types can now be accessed through miden_client_cli::client::*

I think it might be better to do pub use miden_client::* unless there is a good reason not to (sorry I didn't specify more in my previous comment). The alternative would be to do a curated set of re-exports at the top-level and the rest under a namespace.
Otherwise, if you wanted to refer to the Client type you'd have to do miden_client_cli::client::Client which is a bit redundant and doesn't add much in terms of code organization.

@igamigo
Copy link
Collaborator

igamigo commented Jan 16, 2026

As mentioned in 0xMiden/miden-faucet#196, we should probably look into changing the target of this branch to next, no?

@Keinberger
Copy link
Collaborator Author

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

Added pub use miden_client as client. All miden-client types can now be accessed through miden_client_cli::client::*

I think it might be better to do pub use miden_client::* unless there is a good reason not to (sorry I didn't specify more in my previous comment). The alternative would be to do a curated set of re-exports at the top-level and the rest under a namespace. Otherwise, if you wanted to refer to the Client type you'd have to do miden_client_cli::client::Client which is a bit redundant and doesn't add much in terms of code organization.

You are absolutely right! And sorry, I should have probably thought about this myself. Cleaner imports are definitely preferred, made the change!

feat(cli): add silent initialization for CliClient

test(cli): refactor tests not to use global resources

test(cli): modify from_system_user_config CliClient tests  to run serially

test(cli): modify from_system_user_config tests to effectively assert global and local config prioritization

test(cli): add silent initialization tests for from_system_user_config
feat(cli): add ConfigNotFound error variant for better error handling

feat(cli): re-export miden_client as client module

docs(cli): use doc-links for type references in documentation

fix(cli): update test to call from_system_user_config
@Keinberger Keinberger changed the base branch from main to next January 19, 2026 16:56
@Keinberger
Copy link
Collaborator Author

@igamigo I changed the base to next, let me know if it looks good and the PR is good to go :)

Copy link
Collaborator

@igamigo igamigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for changing the base branch.

Unless @mmagician wants to take a look, I think we can merge

Copy link
Contributor

@mmagician mmagician left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I think we can merge, and tackle any potential renamings in a follow-up (if there's consensus) so as to not stall this PR.

Comment on lines +58 to +61
help(
"Run `{} init` command to create a configuration file.",
client_binary_name().display()
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is CliConfig::default() still desirable to have? If so, which context would it be used in? If not, let's remove impl Default for CliConfig

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default is required by the Provider trait implementation used with figment (see line 71). The implementation provides sensible defaults for relative paths that get resolved when loading from a directory. I've added a doc comment clarifying this usage.

Comment on lines 259 to 267
/// Gets a reference to the inner `Client<CliKeyStore>`.
pub fn inner(&self) -> &miden_client::Client<CliKeyStore> {
&self.0
}

/// Gets a mutable reference to the inner `Client<CliKeyStore>`.
pub fn inner_mut(&mut self) -> &mut miden_client::Client<CliKeyStore> {
&mut self.0
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we have DerefMut & Deref implemented, are these two methods needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Removed inner() and inner_mut() since Deref / DerefMut provide the same functionality. Kept into_inner() since it consumes the wrapper (can't be done via deref).

mod config;
pub type CliKeyStore = FilesystemKeyStore;

/// A Client configured using the CLI's system user configuration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally find the naming "system user configuration", but also don't have a better alternative to propose right now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't love it either but not sure what we could rename it to

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally considered alternatives like from_cli_config(), from_environment(), and new().

I think the current name, while verbose, most accurately describes what happens: loading config from the system's user configuration locations (local .miden or global ~/.miden).

Open to suggestions if you have a better alternative!

/// # Ok(())
/// # }
/// ```
pub fn from_system() -> Result<Self, CliError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here, from_system() doesn't suggest that this is the to-go recommended constructor.
Here, perhaps default() could be used (also see my other comment re: the default constructor)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about discoverability. The challenge with using default() is that it's already used to create blank configs (in init.rs:167), and having it perform filesystem I/O would be surprising. Since users are expected to go through CliClient::from_system_user_config() rather than CliConfig::from_system() directly, I kept the current naming.

Open to alternatives if you have ideas!

@igamigo
Copy link
Collaborator

igamigo commented Jan 21, 2026

@Keinberger could you open an issue for tracking potentially renaming or refactoring the CliConfig/CliClient constructors as @mmagician suggested? Then I think we can go ahead and merge this

chore(cli): clarify Default impl usage for CliConfig
@Keinberger
Copy link
Collaborator Author

@Keinberger could you open an issue for tracking potentially renaming or refactoring the CliConfig/CliClient constructors as @mmagician suggested? Then I think we can go ahead and merge this

Creates this issue: #1686

@igamigo
Copy link
Collaborator

igamigo commented Jan 21, 2026

Merging this as the test that times out is being tackled on a separate PR and is unrelated to these changes

@igamigo igamigo merged commit f7ba290 into next Jan 21, 2026
27 of 28 checks passed
@igamigo igamigo deleted the kbg/feat/cli-client branch January 21, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Client::from_system_user_config() helper function

5 participants